﻿using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Threading;
using System.ServiceModel.Channels;
using PSE.ProcessManager.Common.Process;
using System.ServiceModel.Description;
using PSE.ProcessManager.Common.Service;
using System.ServiceModel.Security.Tokens;
using PSE.ProcessManager.Entity;
using PSE.Framework.ErrorLogging;
using PSE.Framework.Configuration;

namespace PSE.ProcessManager.Common.Gateway
{
    public class SenderGateway : IDisposable
    {
        #region Properties

        /// <summary>
        /// Instância que recebe os callbacks.
        /// </summary>
        protected InstanceListenerCallback _instanceCallback;

        /// <summary>
        /// Endereço do ReceiverGateway
        /// </summary>
        public string Address { get; set; }

        /// <summary>
        /// Protocolo de comunicação com o ReceiverGateway
        /// </summary>
        public Protocol Protocol { get; set; }

        /// <summary>
        /// Proxy utilizado na comunicação com o processo.
        /// </summary>
        private IListenerService _proxy;

        /// <summary>
        /// Indica se o Gateway esta conectado ou não.
        /// </summary>
        public bool IsConnected { get; set; }

        //public SessionInformation UserCredential { get; set; }
        public ClientCredentials UserCredential { get; set; }

        protected ServiceProxyFactory<IListenerService> _serviceProxyfactory;

        private const int _connectedMessageId = 6;

        /// <summary>
        /// Proprieda que armazena o tipo de TokenParameters que será utilizado para 
        /// </summary>
        public SecurityTokenParameters TokenParameters
        {
            get;
            set;
        }

        #endregion

        public SenderGateway()
        {
            //Cria uma nova instância do objeto que receberá a chamada Callback.
            _instanceCallback = new InstanceListenerCallback();
            _instanceCallback.OnReceiveMessage += new ServiceEvents.MessageReceived(_instanceCallback_OnReceiveMessage);

            //UserCredential = new SessionInformation(false);
            //UserCredential.TokenId = Guid.NewGuid().ToString();            
        }

        void _instanceCallback_OnReceiveMessage(BaseMessage message)
        {
            if (OnReceiveMessage != null)
                OnReceiveMessage(message);
        }

        #region Events

        /// <summary>
        /// Evento disparado ao conectar com sucesso.
        /// </summary>
        public event EventHandler Connected;
        /// <summary>
        /// Evento disparado ao desconectar com sucesso.
        /// </summary>
        public event EventHandler Disconnected;
        /// <summary>
        /// Evento disparado ao ocorrer uma falta de comunicação.
        /// </summary>
        //public event EventHandler OnFaultThrow;
        /// <summary>
        /// Evento disparado ao receber uma mensagem Callback do ReceiverGateway
        /// </summary>
        public event ServiceEvents.MessageReceived OnReceiveMessage;
        #endregion


        //void SenderGateway_OnReceivedMessage(BaseMessage message)
        //{
        //    switch (message.MessageId)
        //    {
        //        case _connectedMessageId:
        //            if (Connected != null)
        //                Connected(this, new EventArgs());
        //            break;
        //    }
        //}

        #region Connect e Disconnect

        /// <summary>
        /// Efetua uma conexão com outro gateway.
        /// </summary>
        public void Connect()
        {
            //Exception armazenada para o caso de houver falha na conexão.
            Exception inner = null;

            //Tenta, 3 vezes, se conectar com o Receiver.
            //É possível que o Receiver de um processo ainda esteja subindo.
            int iteration = 0;
            while ((!IsConnected) && iteration < 3)
            {
                try
                {
                    //Abre o canal de comunicação.
                    IListenerService proxy = OpenChannel(true);
                    proxy.Connect();
                    IsConnected = true;
                }
                catch (Exception ex)
                {
                    Thread.Sleep(1000);
                    iteration++;
                    IsConnected = false;
                    inner = ex;
                }
            }

            VerifyConnection(inner);

            this.StartKeepAliveTimer();

            if (Connected != null)
                Connected(this, new EventArgs());
        }

        /// <summary>
        /// Conecta com o outro Gateway, passando um id de mensagem que deseja escutar.
        /// </summary>
        /// <param name="messageId"></param>
        public void Connect(int processId, string clientIdentifier)
        {
            //Exception armazenada para o caso de houver falha na conexão.
            Exception inner = null;

            //Tenta, por 3 vezes, se conectar com o Receiver.
            //É possível que o Receiver de um processo ainda esteja subindo.
            int iteration = 0;
            while ((!IsConnected) && iteration < 3)
            {
                try
                {
                    //Abre o canal de comunicação.
                    IListenerService proxy = OpenChannel(true);
                    proxy.ConnectToProcess(processId, clientIdentifier);
                    IsConnected = true;
                }
                catch (Exception ex)
                {
                    Thread.Sleep(1000);
                    iteration++;
                    IsConnected = false;
                    inner = ex;
                }
            }

            VerifyConnection(inner);

            this.StartKeepAliveTimer();

            if (Connected != null)
                Connected(this, new EventArgs());
        }
        /// <summary>
        /// Conecta com o outro Gateway, passando um id de mensagem que deseja escutar.
        /// </summary>
        /// <param name="messageId"></param>
        public void BeginConnect(int processId, string clientIdentifier)
        {
            // A conexão assíncrona foi removia devido a complexidade do tratamento de erros
            this.Connect(processId, clientIdentifier);

            ////Exception armazenada para o caso de houver falha na conexão.
            //Exception inner = null;

            ////Abre o canal de comunicação.
            //IListenerService proxy = OpenChannel();

            ////Tenta, por 3 vezes, se conectar com o Receiver.
            ////É possível que o Receiver de um processo ainda esteja subindo.
            //int iteration = 0;
            //while ((!IsConnected) && iteration < 3)
            //{
            //    try
            //    {
            //        proxy.BeginConnectToProcess(processId, clientIdentifier, new AsyncCallback(BeginConnectAssyncResult), null);
            //        IsConnected = true;
            //    }
            //    catch (Exception ex)
            //    {
            //        Thread.Sleep(1000);
            //        iteration++;
            //        IsConnected = false;
            //        inner = ex;
            //    }
            //}

            //VerifyConnection(inner);
        }


        /// <summary>
        /// Conecta com o outro Gateway, enviando uma lista de mensagens que deseja escutar.
        /// </summary>
        /// <param name="messageIds"></param>
        public void Connect(List<int> messageIds)
        {
            //Exception armazenada para o caso de houver falha na conexão.
            Exception inner = null;

            //Abre o canal de comunicação.
            IListenerService proxy = OpenChannel();

            //Tenta, 3 vezes, se conectar com o Receiver.
            //É possível que o Receiver de um processo ainda esteja subindo.
            int iteration = 0;
            while ((!IsConnected) && iteration < 3)
            {
                try
                {
                    if (proxy == null ||
                        ((IChannel)proxy).State != CommunicationState.Created ||
                        ((IChannel)proxy).State != CommunicationState.Opening ||
                        ((IChannel)proxy).State != CommunicationState.Opened)
                    {
                        proxy = OpenChannel();
                    }

                    proxy.ConnectToListenMessages(messageIds);
                    IsConnected = true;
                }
                catch (Exception ex)
                {
                    Thread.Sleep(1000);
                    iteration++;
                    IsConnected = false;
                    inner = ex;
                }
            }

            VerifyConnection(inner);

            this.StartKeepAliveTimer();

            if (Connected != null)
                Connected(this, new EventArgs());
        }
        /// <summary>
        /// Verifica se a conexão foi efetuada com sucesso, ou gera uma exceção com o erro.
        /// </summary>
        /// <param name="inner"></param>
        private void VerifyConnection(Exception inner)
        {
            //Se não conseguir conectar, dispara uma exception.
            if (IsConnected == false)
            {
                CloseChannel();
                throw inner;
            }
        }

        /// <summary>
        /// Disconecta o gateway.
        /// </summary>
        public void Disconnect()
        {
            if (IsConnected)
            {
                try
                {
                    //Envia uma mensagem de desconexão para o ReceiverGateway
                    IListenerService proxy = OpenChannel();

                    proxy.Disconnect();
                }
                finally
                {
                    //Fecha o canal de comunicação.
                    CloseChannel();

                    IsConnected = false;
                }

                if (Disconnected != null)
                    Disconnected(this, new EventArgs());
            }
        }

        #endregion

        private Timer _keepAliveTimer = null;

        private void StartKeepAliveTimer()
        {
            // Inicia o timer do keep alive, executa a cada 5 minutos
            _keepAliveTimer = new Timer(new TimerCallback(KeepAliveTimerCallback), null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
        }

        private void KeepAliveTimerCallback(object state)
        {
            try
            {
                _proxy.KeepAlive();
            }
            catch
            {
                if (_keepAliveTimer != null)
                {
                    _keepAliveTimer.Dispose();
                    _keepAliveTimer = null;
                }
            }
        }

        #region Criação e Destruição do Canal de comunicação.
        /// <summary>
        /// Abre o canal de comunicação com o outro Receiver.
        /// </summary>
        /// <returns>Retorna o proxy de acesso.</returns>
        private IListenerService OpenChannel(bool newChannel)
        {
            try
            {
                if (String.IsNullOrEmpty(Address))
                    //TODO: Implementar tratamento de erro quando não houver um endereço definido.
                    throw new Exception("Address must be informed.");

                if (_proxy == null || newChannel /*|| 
                    ((ICommunicationObject)_proxy).State != CommunicationState.Created || 
                    ((ICommunicationObject)_proxy).State != CommunicationState.Opened*/)
                {
                    CloseChannel(false);

                    EndpointAddress endpoint = new EndpointAddress(Address);
                    _serviceProxyfactory = new ServiceProxyFactory<IListenerService>();
                    _proxy = CreateProxy(endpoint);

                    //(_proxy as ICommunicationObject).Closing += new EventHandler(SenderGateway_Closing);
                    //(_proxy as ICommunicationObject).Faulted += new EventHandler(SenderGateway_Faulted);
                }

                return _proxy;
            }
            catch (Exception ex)
            {
                throw new CommunicationException(String.Format("Could not connect to address '{0}'. See inner exceptions for more details.", Address), ex);
            }
        }

        /// <summary>
        /// Método virtual responsável pela criação do proxy de acesso ao serviço
        /// </summary>
        /// <param name="endpoint"></param>
        /// <returns></returns>
        protected virtual IListenerService CreateProxy(EndpointAddress endpoint)
        {
            return _serviceProxyfactory.GetDuplexChannelProxy(this.UserCredential, _instanceCallback, endpoint, TokenParameters, true);
        }

        //void SenderGateway_Faulted(object sender, EventArgs e)
        //{
        //    if (OnFaultThrow != null)
        //        OnFaultThrow(sender, e);
        //}

        //void SenderGateway_Closing(object sender, EventArgs e)
        //{

        //}

        /// <summary>
        /// Abre o canal de comunicação com o outro Receiver.
        /// </summary>
        /// <returns>Retorna o proxy de acesso.</returns>
        private IListenerService OpenChannel()
        {
            return OpenChannel(false);

        }

        /// <summary>
        /// Fecha o canal de comunicação com o outro Gateway
        /// </summary>
        private void CloseChannel()
        {
            this.CloseChannel(true);
        }

        private void CloseChannel(bool disconnect)
        {
            if (_keepAliveTimer != null)
            {
                _keepAliveTimer.Dispose();
                _keepAliveTimer = null;
            }

            if (_serviceProxyfactory != null)
            {
                _serviceProxyfactory.Dispose();
                _serviceProxyfactory = null;
            }

            if (_proxy != null)
            {
                try
                {
                    if (((ICommunicationObject)_proxy).State != CommunicationState.Faulted)
                        ((ICommunicationObject)_proxy).Close();
                }
                catch
                {
                    ((ICommunicationObject)_proxy).Abort();
                }
            }
            _proxy = null;

            if (disconnect)
                this.IsConnected = false;
        }

        #endregion


        #region IDisposable Members

        public void Dispose()
        {
            //Fecha o canal de comunicação.
            CloseChannel();
        }

        #endregion

        /// <summary>
        /// Envia uma mensagem para o ReceiverGateway conectado.
        /// </summary>
        /// <param name="message"></param>
        public void SendMessage(PSE.ProcessManager.Common.Gateway.BaseMessage message)
        {
            if (IsConnected)
            {
                try
                {
                    IListenerService proxy = OpenChannel();

                    System.ServiceModel.Channels.Message serviceMessage = MessageCreator.CreateMessage(message, ListenerService.Actions.SendMessage);

                    proxy.SendMessage(serviceMessage);
                }
                catch
                {
                    CloseChannel();
                    throw;
                }

            }
            else
                throw new CommunicationException("Gateway not connected.");
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="message"></param>
        public void BeginSendMessage(PSE.ProcessManager.Common.Gateway.BaseMessage message)
        {
            if (IsConnected)
            {
                try
                {
                    IListenerService proxy = OpenChannel();

                    System.ServiceModel.Channels.Message serviceMessage = MessageCreator.CreateMessage(message, ListenerService.Actions.SendMessage);

                    proxy.BeginSendMessage(serviceMessage, new AsyncCallback(AssyncResult), proxy);
                }
                catch
                {
                    CloseChannel();
                    throw;
                }

            }
            else
                throw new CommunicationException("Gateway not connected.");
        }

        private void AssyncResult(IAsyncResult result)
        {
            try
            {
                //IListenerService proxy = (IListenerService)result.AsyncState;
                IListenerService proxy = OpenChannel();
                proxy.EndSendMessage(result);
            }
            catch (Exception ex)
            {
                ErrorLogger.WriteLog(LogType.Error, new Exception("An error ocurred sending message.", ex));
            }
        }

        //private void BeginConnectAssyncResult(IAsyncResult result)
        //{
        //    IListenerService proxy = OpenChannel();
        //    proxy.EndConnectToProcess(result);

        //    this.StartKeepAliveTimer();

        //    if (Connected != null)
        //        Connected(this, new EventArgs());
        //}

        public void Close()
        {
            CloseChannel();
        }

        //public ProcessStatus RetrieveProcessStatus(int processId)
        //{
        //    IListenerService cli = OpenChannel();
        //    return cli.RetrieveProcessStatus(processId);
        //}

        public void BeginSendMessage<T>(Message<T> message)
        {
            if (IsConnected)
            {
                try
                {
                    IListenerService proxy = OpenChannel();

                    System.ServiceModel.Channels.Message serviceMessage = MessageCreator.CreateMessage((IMessage)message, ListenerService.Actions.SendMessage);

                    proxy.BeginSendMessage(serviceMessage, new AsyncCallback(AssyncResult), proxy);
                }
                catch
                {
                    CloseChannel();
                    throw;
                }

            }
            else
                throw new CommunicationException("Gateway not connected.");
        }

        public bool IsProcessRunning(int processId)
        {
            if (IsConnected)
            {
                try
                {
                    IListenerService proxy = OpenChannel();

                    return proxy.IsProcessExecuting(processId);
                }
                catch
                {
                    CloseChannel();
                    throw;
                }
            }
            else
                throw new CommunicationException("Gateway not connected.");
        }

        public int GetProcessPid(int processId)
        {
            if (IsConnected)
            {
                try
                {
                    IListenerService proxy = OpenChannel();

                    return proxy.GetProcessPid(processId);
                }
                catch
                {
                    CloseChannel();
                    throw;
                }
            }
            else
                throw new CommunicationException("Gateway not connected.");
        }

        public List<LogEntity> RetrieveLogList(int processId)
        {
            if (IsConnected)
            {
                try
                {
                    IListenerService proxy = OpenChannel();

                    return proxy.RetrieveLogList(processId);
                }
                catch
                {
                    CloseChannel();
                    throw;
                }
            }
            else
                throw new CommunicationException("Gateway not connected.");
        }

        public LogEntity RetrieveLastProcessLog(int processId)
        {
            if (IsConnected)
            {
                try
                {
                    IListenerService proxy = OpenChannel();

                    return proxy.RetrieveLastProcessLog(processId);
                }
                catch
                {
                    CloseChannel();
                    throw;
                }
            }
            else
                throw new CommunicationException("Gateway not connected.");
        }
    }
}
